from .utils import RTOL, ATOL, CfDict
from .utils import equals as utils_equals
from .units import Units

# ====================================================================
#
# Space object
#
# ====================================================================

class Space(CfDict):
    '''

Completely describe a field's coordinate system (space).

It contains the dimension constructs, auxiliary coordinate constructs,
cell measure constructs and transform constructs defined by the CF
data model.

The space is a dictionary-like object whose key/value pairs identify
and store the coordinate and cell measure constructs which describe
it.

The dimensionality of the space's components and its transforms are
stored as attributes.

'''

    def __init__(self, *args, **kwargs):
        '''

**Initialization**

The `dimension_sizes`, `dimensions` and `transforms` attributes are
automatically initialized.

:Parameters:

    args, kwargs
        Keys and values are initialized exactly as for a built-in
        dict. Keys are coordinate and cell measure construct
        identifiers (such as dim1, aux0, and cm2) and values are
        coordinate and cell measure instances as appropriate.

'''
        super(Space, self).__init__(*args, **kwargs)

        self.dimension_sizes = {}
        self.dimensions = {}
        self.transforms = CfDict()
    #--- End: def

    def __repr__(self):
        '''
x.__repr__() <==> repr(x)

'''
        return '<CF %s: %s>' % (self.__class__.__name__, 
                                tuple(self.dimension_sizes.values()))
    #--- End: def

    def __str__(self):
        '''
x.__str__() <==> str(x)

'''
        def _print_coord(space, key, dimension_coord):
            '''Private function called by __str__'''
            try: 
                variable = space[key]
            except KeyError: 
                size = space.dimension_sizes[key]
                try:
                    name = space.nc_dimensions[key]
                except (KeyError, AttributeError):
                    name = key
                return '%s(%d)' % (name, size)
            #--- End: try

            # Still here?
            if dimension_coord:
                # Dimension coordinate
                shape = variable.shape
                if shape:
                    shape = '(%d)' % variable.size
                else:
                    shape = ''
                name = variable.name(ncvar=True, default=key)
            else:
                # Auxiliary coordinate
                try:
                    name = variable.name(ncvar=True, default=key)
                except TypeError:
                    name = key
                shape = space.dimensions[key][:]
                for i in xrange(len(shape)):
                    try:
                        shape[i] = space[shape[i]].name(ncvar=True, default=shape[i])
                    except KeyError:
                        shape[i] = space.nc_dimensions[shape[i]]
                shape = str(tuple(shape)).replace("'", "")
                shape = shape.replace(',)', ')')
            #--- End: if
            x = [name]
            x.append(shape)

            try:
                variable.compress
            except AttributeError:
                if variable.Units and variable.Units != Units('1'):
                    units = variable.units
                else:
                    units = ''

                ndim = variable.ndim
                size = variable.size

                o_brackets = '[' * ndim
                c_brackets = ']' * ndim

                first = variable.first_datum
                last  = variable.last_datum

                if variable.dtype.kind == 'S':
                    # String valued coordinate's first and last values
                    if size == 1:
                        x.append(" = %s'%s'%s %s" % (o_brackets, first,
                                                     c_brackets, units))
                    elif size >= 3:
                        x.append(" = %s'%s', ..., '%s'%s %s" %  (o_brackets, first,
                                                                 last, c_brackets,
                                                                 units))
                    else:
                        x.append(" = %s'%s', '%s'%s %s" %  (o_brackets, first,
                                                            last, c_brackets,
                                                            units))
                else:
                    # Numeric valued coordinate's first and last values
                    if size == 1:
                        x.append(' = %s%.7g%s %s' % (o_brackets, first,
                                                     c_brackets, units))
                    elif size >= 3:
                        x.append(' = %s%.7g, ..., %.7g%s %s' % (o_brackets, first,
                                                                last, c_brackets,
                                                                units))
                    else:
                        x.append(' = %s%.7g, %.7g%s %s' % (o_brackets, first,
                                                           last, c_brackets,
                                                           units))
                        
            else:
                x.append(' -> compressed ')
                compressed = []
                for unc in space[key].compress:
                    shape = str(unc.size)
                    compressed.append(unc.name(ncvar=True,
                                               default='unc')+'('+shape+')')
                x.append(', '.join(compressed))

            return ''.join(x)
        #--- End: def

        string = ['Dimensions      : ']
        x = []
        for dim in sorted(self.dimension_sizes):
            x.append(_print_coord(self, dim, True))
        string.append('\n                : '.join(x))
        string.append('\n')

        string.append('Auxiliary coords: ')
        x = []
        for aux in sorted(self.get_keys('^aux')):
            x.append(_print_coord(self, aux, False))
        string.append('\n                : '.join(x))
        string.append('\n')

        # Cell measures
        cell_measures = self.get_keys('^cm')
        if cell_measures:
            string.append('Cell measures   : ')
            x = []
            for cm in sorted(cell_measures):
                x.append(_print_coord(self, cm, False))
            string.append('\n                : '.join(x))
            string.append('\n')

        # Virtual auxiliary coordinates
        if self.transforms:
            transforms = self.transforms.values()
            string.append('Virtual coords  : ')
            x = []
            for transform in transforms:
                if not transform.isgrid_mapping:
                    x.append(repr(transform))
            #-- End: for
            string.append('\n                : '.join(x))
        #--- End: if

        return ''.join(string)
    #--- End: def

    # ----------------------------------------------------------------
    # Attribute: dimension_sizes
    # ----------------------------------------------------------------
    @property
    def dimension_sizes(self):
        '''
        
A dictionary of the space's dimensions and their sizes.

**Examples**

>>> s.dimension_sizes
{'dim0': 1,
 'dim1': 73,
 'dim2': 96}

'''
        return self._dimension_sizes
    #--- End: def
    @dimension_sizes.setter
    def dimension_sizes(self, value):
        self._dimension_sizes = value

    # ----------------------------------------------------------------
    # Attribute: dimensions
    # ----------------------------------------------------------------
    @property
    def dimensions(self):
        '''
                
A dictionary of the space's components (including the field's data
array) and their dimensions.

>>> s.dimensions
{'aux0': ['dim1', 'dim2'],
 'aux1': ['dim2', 'dim1'],
 'cm0' : ['dim1', 'dim2'],
 'data': ['dim0', 'dim1', 'dim2'],
 'dim0': ['dim0'],
 'dim1': ['dim1'],
 'dim2': ['dim2']}

'''
        return self._dimensions
    #--- End: def
    @dimensions.setter
    def dimensions(self, value):
        self._dimensions = value

    # ----------------------------------------------------------------
    # Attribute: transforms
    # ----------------------------------------------------------------
    @property
    def transforms(self):
        '''

A dictionary-like object of the space's transforms and their identifiers.

**Examples**

>>> s.transforms
{'trans0': <CF Transform: ocean_sigma_z_coordinate>,
 'trans1': <CF Transform: rotated_latitude_logitude>}
>>> isinstance(s.transforms, cf.CfDict)
True

'''
        return self._transforms
    #--- End: def
    @transforms.setter
    def transforms(self, value):
        self._transforms = value

    def analyse(self):
        '''

'''
        a = {}

        # ------------------------------------------------------------
        # Map each dimension's identity to its space identifier, if
        # such a mapping exists.
        #
        # For example:
        # >>> id_to_dim
        # {'time': 'dim0', 'height': dim1'}
        # ------------------------------------------------------------
        id_to_dim = {}

        # ------------------------------------------------------------
        # For each dimension that is identified by a 1-d auxiliary
        # coordinate, map its dimension's its space identifier.
        #
        # For example:
        # >>> id_to_aux
        # {'region': 'aux0'}
        # ------------------------------------------------------------
        id_to_aux = {}

        # ------------------------------------------------------------
        # Map each dimension's identity to the coordinate which
        # provides that identity.
        #
        # For example:
        # >>> id_to_coord
        # {'time': <CF Coordinate: time(12)>}
        # ------------------------------------------------------------
        id_to_coord = {}

        dim_to_coord = {}
        aux_to_coord = {}

        # ------------------------------------------------------------
        #
        # ------------------------------------------------------------
        aux_coords = {}
        aux_coords['N-d'] = {}

        cell_measures = {}
        cell_measures['N-d'] = set()

        # ------------------------------------------------------------
        # List the dimensions which are undefined, in that no unique
        # identity can be assigned to them.
        #
        # For example:
        # >>> undefined_dims
        # ['dim2']
        # ------------------------------------------------------------
        undefined_dims = []

        # ------------------------------------------------------------
        #
        # ------------------------------------------------------------
        warnings = []

        for dim in self.dimension_sizes:

            # Find this dimension's 1-d and N-d auxiliary coordinates
            aux_coords_dim = self.aux_coords(dim)
            aux_coords[dim]        = {}
            aux_coords[dim]['1-d'] = {}
            aux_coords[dim]['N-d'] = {}
            for aux, coord in aux_coords_dim.iteritems():
                if coord.ndim > 1:
                    aux_coords['N-d'][aux] = coord
                    aux_coords[dim]['N-d'][aux] = coord
                else:
                    aux_coords[dim]['1-d'][aux] = coord
            #--- End: for

            # Find this dimension's 1-d and N-d cell measures
            cell_measures_dim = self.cell_measures(dim)
            cell_measures[dim]        = {}
            cell_measures[dim]['1-d'] = {}
            cell_measures[dim]['N-d'] = {}
            for cm, cell_measure in cell_measures_dim.iteritems():
                if cell_measure.ndim > 1:
                    cell_measures['N-d']      = self[cm]
                    cell_measures[dim]['N-d'] = self[cm]
                else:
                    cell_measures[dim]['1-d'] = self[cm]
            #--- End: for

            if dim in self:
                # This dimension of the space has a dimension
                # coordinate
                dim_coord = self[dim]
                identity = dim_coord.identity()
                if identity is not None and dim_coord.hasData:
                    if identity in id_to_dim:
                        warnings.append(
                            "Space has more than one '%s' dimension" % identity)

                    id_to_dim[identity]   = dim
                    id_to_coord[identity] = dim_coord
                    dim_to_coord[dim]     = dim_coord
                    continue

            elif len(aux_coords[dim]['1-d']) == 1:
                # This dimension of the space does not have a
                # dimension coordinate but it does have exactly one
                # 1-d auxiliary coordinate, so that will do.
                aux       = list(aux_coords[dim]['1-d'])[0]
                aux_coord = self[aux]
                
                identity = aux_coord.identity()
                if identity is not None and aux_coord.hasData:
                    if identity in id_to_dim:
                        warnings.append(
                            "Space has more than one '%s' dimension" % identity)

                    id_to_aux[identity]   = aux
                    id_to_dim[identity]   = dim
                    id_to_coord[identity] = aux_coord
                    aux_to_coord[dim]     = aux_coord
                    continue
            #--- End: if

            # Still here? Then this dimension is undefined
            undefined_dims.append(dim)
        #--- End: for

        # ------------------------------------------------------------
        # Invert the mapping between dimensions and identities
        # ------------------------------------------------------------
        dim_to_id = dict((v, k) for k, v in id_to_dim.iteritems())

        return {'aux_coords'    : aux_coords,
                'aux_to_coord'  : aux_to_coord,
                'cell_measures' : cell_measures,
                'dim_to_coord'  : dim_to_coord,
                'dim_to_id'     : dim_to_id,
                'id_to_aux'     : id_to_aux,
                'id_to_coord'   : id_to_coord,
                'id_to_dim'     : id_to_dim,
                'undefined_dims': undefined_dims,
                'warnings'      : warnings,                
                }    
    #--- End def 

    def direction(self, dim):
        '''

Return True if a dimension is increasing, otherwise return False.

A dimension is considered to be increasing if its dimension coordinate
values are increasing in index space or if it has no dimension
coordinate.

The direction is taken directly from the appropriate coordinate's Data
object, if available. (This is because we can assume that the space
has been finalized.)

:Parameters:

    dim : str
        The identifier of the dimension (such as 'dim0').

:Returns:

    out : bool
        Whether or not the dimension is increasing.
        
**Examples**

>>> s.dimension_sizes
{'dim0': 3, 'dim1': 1, 'dim2': 2, 'dim3': 2, 'dim4': 99}
>>> s.dimensions
{'dim0': ['dim0'],
 'dim1': ['dim1'],
 'aux0': ['dim0'],
 'aux1': ['dim2'],
 'aux2': ['dim3'],
}
>>> s['dim0'].array
array([  0  30  60])
>>> s.direction('dim0')
True
>>> s['dim1'].array
array([15])
>>> s['dim1'].bounds.array
array([  30  0])
>>> s.direction('dim1')
False
>>> s['aux1'].array
array([0, -1])
>>> s.direction('dim2')
True
>>> s['aux2'].array
array(['z' 'a'])
>>> s.direction('dim3')
True
>>> s.direction('dim4')
True

'''
        if dim not in self:
            return True
        
        coord = self[dim]

        if not coord.hasData:
            return True

        data = coord.Data

        if data.isscalar:
            return data.direction

        return data.direction.values()[0]
    #--- End: def

    def map_dims(self, other):
        '''

Map the dimnesions of two spaces.

:Return:

   out : dict

'''
        s = self.analyse()
        t = other.analyse()
        
        out = {}
        
        for identity, dim in s['id_to_dim'].iteritems():
            if identity in t['id_to_dim']:
                out[dim] = t['id_to_dim'][identity]
        #--- End: for

        return out
    #--- End: def

    def new_dimension(self):
        '''
Return a new

'''
        dimension_sizes = set(self.dimension_sizes)

        N       = len(dimension_sizes)
        new_dim = 'dim%d' % N

        while new_dim in dimension_sizes:
            N      += 1
            new_dim = 'dim%d' % N
        #--- End: while

        return new_dim
    #--- End: def

    def new_auxiliary(self):
        '''
Return a new 

'''
        aux_keys = self.get_keys('^aux')

        N       = len(aux_keys)
        new_aux = 'aux%d' % N

        while new_aux not in aux_keys:
            N      += 1
            new_aux = 'aux%d' % N
        #--- End: while

        return new_aux
    #--- End: def

    def new_transform(self):
        '''
Return a new 

'''
        if not self.transforms:
            return 'trans0'
        
        transforms = set(self.transforms)

        N             = len(transforms)
        new_transform = 'trans%d' % N

        while new_transform not in transforms:
            N += 1
            new_transform = 'trans%d' % N
        #--- End: while

        return new_transform
    #--- End: def

    def insert_coordinate(self, coord, dim_name_map=None, 
                          dim=False, aux=False, dimensions=None, 
                          space=None, key=None):
        '''

Insert a new dimension or auxiliary coordinate to the space in place.

:Parameters:

    coord : Coordinate
        The new coordinate.

    dim_name_map : dict, optional

    dim : bool, optional
        True if the new coordinate is to be a dimension coordinate.

    aux : bool, optional
        True if the new coordinate is to be an auxiliary coordinate.

    dimensions : list
        The ordered dimensions of the new coordinate. Ignored if the
        coordinate is a dimension coordinate. Required if the
        coordinate is an auxiliary coordinate.

    space : Space, optional        
        Provide a space which contains both the new coordinate and its
        transforms, thus enabling transforms of the new coordinate to
        be included. By default, transforms of the new coordinate are
        not included.

    key : str, optional
        The identifier for the new coordinate. By default a unique
        identifier will be generated.

:Returns:

    None

**Examples**

>>>

'''
        if dim:
            if not key:
                key = self.new_dimension()

            dimensions                = [key]
            self.dimension_sizes[key] = coord.size
        elif aux:
            if not key:
                key = self.new_auxiliary()
            if not dimensions:
                raise ValueError(
"Must specify the dimensions for a new auxiliary coordinate")
        else:
            raise ValueError("Must specify the role of the new coordinate")
        #--- End: if

        self.dimensions[key] = dimensions[:]
            
        if dim_name_map is None:
            pass

        coord = coord.copy()
        coord.Data.change_dimension_names(dim_name_map)

        if hasattr(coord, 'transforms'):
            # NEEDS REDOING - probably wrong

            if not space:
                del coord.transforms
            else:
                transform_id = coord.transforms
                if space.transforms[transform_id].isgrid_mapping:
                    # Keep grid mappings
                    transN = self.new_transform()
                    if space.transforms[transform_id] not in self.transforms:
                        self.transforms[transN] = \
                            space.transforms[transform_id].copy()
                        
                    coord.transforms = [transN]
                else:
                    # Throw away formula_terms (for now ...?)
                    del coord.transforms
        #--- End: if

        self[key] = coord

        return coord
    #--- End: def

    def pop(self, key, *default):
        '''

Remove a coordinate or cell measure from the space in place and return
it.

:Parameters:

    key : str

:Returns:

    out : Coordinate or CellMeasure

**Examples**

>>> s.pop('dim0')
>>> s.pop('aux1')
>>> s.pop('cm2')

'''
        if key in self:
            del self.dimensions[key]
            return self._dict.pop(key)

        elif default:
            return default[0]

        raise KeyError(" asdasdasdsad asd asdad 45928")
    #--- End: def


    def remove_coordinate(self, key):
        '''

Remove a coordinate from the space in place.

:Parameters:

    key : str
        The coordinate's identifier.

:Returns:

    out : Coordinate or CellMeasure

:Raises:

    KeyError :

**Examples**

>>> s.remove_coordinate('dim0')
>>> s.remove_coordinate('aux1')

'''        
        return self.pop(key)
    #--- End: def

    def squeeze(self, dims):
        '''

Remove a size 1 dimension from the space in place.

If the dimension has a dimension coordinate then it is removed, as are
1-d auxiliary coordinates and cell measures which span the
dimension. The dimension is squeezed from multidimensional auxiliary
coordinates and cell measures if they span it.

The dimension is not squeezed from the field's data array if it spans
it, therefore the field's data array may need to be squeezed
concurrently.

:Parameters:

    dims : str or sequence of strs
        The identifier of the dimension to remove.
    
:Returns:

    None

**Examples**

>>> s.dimension_sizes
{'dim0': 12, 'dim1': 73, 'dim2': 1}
>>> s.dimensions
{'data': ['dim0', 'dim1', 'dim2'],
 'aux0': ['dim1', 'dim2'],
 'aux1': ['dim2', 'dim1'],
 'dim0': ['dim0'],
 'dim1': ['dim1'],
 'dim2': ['dim2'],
 'cm0' : ['dim1', 'dim2']}
>>> s.squeeze('dim2')
>>> s.dimension_sizes
{'dim0': 12, 'dim1': 73}
>>> s.dimensions
{'data': ['dim0', 'dim1', 'dim2'],
 'aux0': ['dim1'],
 'aux1': ['dim1'],
 'dim0': ['dim0'],
 'dim1': ['dim1'],
 'cm0' : ['dim1']}

'''
        if isinstance(dims, basestring):
            dims = (dims,)

        for dim in dims:
            if dim in self.dimension_sizes:
                if self.dimension_sizes[dim] > 1:
                    raise ValueError("Can't squeeze a dimension with size > 1 from %s" %
                                     self.__class__.__name__)
            
                # Remove the dimension
                del self.dimension_sizes[dim]
            else:
                raise ValueError("Can't remove non-existent dimension from %s" %
                                 self.__class__.__name__)
            
            if dim in self:
                self.remove_coordinate(dim)
            
            dimensions = self.dimensions.copy()
            del dimensions['data']
            
            for key, dims in dimensions.iteritems():
                if dim not in dims:
                    continue
            
                if len(dims) == 1:
                    # Remove the dimension's 1-d auxiliary coordinates
                    self.remove_coordinate(key)
                else:
                    # Squeeze the dimension out of N-d auxiliary coordinates
                    axis = dims.index(dim)
                    dims.pop(axis)
                    self[key].squeeze(axis)
            #--- End: for
        #--- End: for
    #--- End: def

    def aux_coords(self, dim=None):
        '''

Return a dictionary whose values are the auxiliary coordinates which
span the given dimension and keys of the space's auxiliary coordinate
identifiers.

:Parameters:

    dim : str, optional
        The identifier of the dimension to be spanned. By default all
        dimensions are considered (so all auxiliary coordinates are
        returned).

:Returns:

    out : dict
        The auxiliary coordinates and their identifiers.

**Examples**

>>> s.dimensions
{'data': ['dim0', 'dim1', 'dim2'],
 'dim0': ['dim0'],
 'dim1': ['dim1'],
 'dim2': ['dim2'],
 'dim3': ['dim3'],
 'aux0': ['dim1', 'dim2'],
 'aux1': ['dim0'],
 'aux2': ['dim2', 'dim1']}
>>> s.aux_coords()
{'aux0': <CF Coordinate: ...>,
 'aux1': <CF Coordinate: ...>,
 'aux2': <CF Coordinate: ...>}
>>> s.aux_coords('dim2')
{'aux0': <CF Coordinate: ...>,
 'aux2': <CF Coordinate: ...>}
>>> s.aux_coords('dim3')
{}

'''       
        out = {}

        if dim is None:            
            for key in self.get_keys('^aux'):
                out[key] = self[key]

        else:
            dimensions = self.dimensions.copy()
            del dimensions['data']
            for key in self.get_keys('^aux'):
                if dim in dimensions[key]:
                    out[key] = self[key]
        #--- End: if

        return out
    #--- End: def

    def cell_measures(self, dim=None):
        '''

Return a dictionary whose values are the cell measures which span the
given dimension and keys of the space's cell measure identifiers.

:Parameters:

    dim : str, optional
        The identifier of the dimension to be spanned. By default all
        dimensions are considered (so all cell measures are returned).

:Returns:

    out : dict
        The cell measures and their identifiers.

**Examples**

>>> s.dimensions
{'data': ['dim0', 'dim1', 'dim2'],
 'dim0': ['dim0'],
 'dim1': ['dim1'],
 'dim2': ['dim2'],
 'cm0' : ['dim1', 'dim2'],
 'cm1' : ['dim1', 'dim2', 'dim3']}
>>> s.cell_measures()
{'cm0': <CF CellMeasure: ...>,
 'cm1': <CF CellMeasure: ...>}
>>> s.cell_measures('dim3')
{'cm1': <CF CellMeasure: ...>}
>>> s.cell_measures('dim0')
{}

'''       
        out = {}

        if dim is None:            
            for key in self.get_keys('^cm'):
                out[key] = self[key]

        else:
            dimensions = self.dimensions.copy()
            del dimensions['data']
            for key in self.get_keys('^cm'):
                if dim in dimensions[key]:
                    out[key] = self[key]
        #--- End: if

        return out
    #--- End: def

    def expand_dims(self, coord=None, size=1):
        '''

Expand the space with a new dimension in place.

The new dimension may by of any size greater then 0.

:Parameters:

    coord : Coordinate, optional
        A dimension coordinate for the new dimension. The new
        dimension's size is set to the size of the coordinate's array.

    size : int, optional
        The size of the new dimension. By default a dimension of size
        1 is introduced. Ignored if `coord` is set.

:Returns:

    None

**Examples**

>>> s.expand_dims()
>>> s.expand_dims(size=12)
>>> c
<CF Coordinate: >
>>> s.expand_dims(coord=c)

'''
        dim = self.new_dimension()

        if coord:
            self[dim] = coord.copy()
            if hasattr(coord, 'size'):
                size = coord.size
        #--- End: if

        self.dimension_sizes[dim] = size
    #--- End: def

    def dump(self, id=None):
        '''
        
Return a string containing a full description of the space.

:Parameters:

    id : str, optional
        Set the common prefix of component names. By default the
        instance's class name is used.

:Returns:

    out : str
        A string containing the description.

**Examples**

>>> x = s.dump()
>>> print s.dump()
>>> print s.dump(id='space1')

'''
        if id is None:
            id = self.__class__.__name__

        try:
            string = ['%s space' % self.name]
        except AttributeError:
            string = ['space']
        string.append(''.ljust(len(string[0]), '-'))

        string.append('%s.dimension_sizes = %s\n' % (id, self.dimension_sizes))
        
        for key in sorted(self.dimensions):
            string.append("%s.dimensions['%s'] = %s" %
                          (id, key, self.dimensions[key]))
        #--- End: for

        string.append('')

        for key in (sorted(self.get_keys('^dim')) + 
                    sorted(self.get_keys('^aux')) + 
                    sorted(self.get_keys('^cm'))):
            string.append(self[key].dump(id="%(id)s['%(key)s']" % locals()))
           
        for key, transform in self.transforms.iteritems():
            string.append(transform.dump(
                    id="%(id)s.transforms['%(key)s']" % locals()))
            string.append('')
        #--- End: for

        return '\n'.join(string)
    #--- End: def

    def equals(self, other, rtol=None, atol=None, traceback=False):
        '''

True if two spaces are equal, False otherwise.

Equality is defined as follows:

    * There is one-to-one correspondence between dimensions and
      dimension sizes between the two spaces.

    * For each space component type (dimension coordinate, auxiliary
      coordinate and cell measures), the set of constructs in one
      space equals that of the other space. The component identifiers
      need not be the same.

    * The set of transforms in one space equals that of the other
      space. The transform identifiers need not be the same.

Equality of numbers is to within a tolerance.

:Parameters:

    other :
        The object to compare for equality.

    atol : float, optional
        The absolute tolerance for all numerical comparisons, By
        default the value returned by the `ATOL` function is used.

    rtol : float, optional
        The relative tolerance for all numerical comparisons, By
        default the value returned by the `RTOL` function is used.

    traceback : bool, optional
        If True then print a traceback highlighting where the two
        instances differ.

:Returns: 

    out : bool
        Whether or not the two instances are equal.

**Examples**

>>> s.equals(t)
True

'''
        if self is other:
            return True
        
        # Check that each instance is the same type
        if type(self) != type(other):
            print("%s: Different types: %s, %s" %
                  (self.__class__.__name__,
                   self.__class__.__name__,
                   other.__class__.__name__))
            return
        #--- End: if

        if (sorted(self.dimension_sizes.values()) != 
            sorted(other.dimension_sizes.values())):
            # There is not a 1-1 correspondence between dimensions and
            # dimension sizes between the two spaces.           
            if traceback:
                print("%s: Different ranks: %s, %s" %
                      (self.__class__.__name__,
                       sorted(self.dimension_sizes.values()),
                       sorted(other.dimension_sizes.values())))
            return
        #--- End: if

        if rtol is None:
            rtol = RTOL()
        if atol is None:
            atol = ATOL()              

        # ------------------------------------------------------------
        # Test the transforms
        # ------------------------------------------------------------
        transform_map = {}
        if not self.transforms:
            if other.transforms:
                # Self doesn't have any transforms but other does
                if traceback:
                    print("%s: Different numbers of transforms" %
                          self.__class__.__name__)
                return
        else:
            if not other.transforms:
                # Other doesn't have any transforms but self does
                if traceback:
                    print("%s: Different numbers of transforms" %
                          self.__class__.__name__)
                return
            #--- End: if

            transforms0 = self.transforms
            transforms1 = other.transforms

            self_keys  = sorted(transforms0.keys())
            other_keys = sorted(transforms1.keys())
            for key0 in self_keys:
                transform0 = transforms0[key0]
                found_match = False
                for key1 in other_keys:
                    if transform0.equals(transforms1[key1], rtol=rtol,
                                         atol=atol, traceback=False):
                        found_match = True
                        other_keys.remove(key1)
                        transform_map[key1] = key0
                        break
                #--- End: for
                if not found_match: 
                    if traceback:
                        print("%s: Different transform: %s" %
                              (self.__class__.__name__,
                               repr(transform0)))
                    return
            #--- End: for
        #--- End: if

        # ------------------------------------------------------------
        # Test the coordinates and cell measures
        # ------------------------------------------------------------
        for coord_type in ('^dim', '^aux', '^cm'):
            self_keys  = sorted(self.get_keys(coord_type))
            other_keys = sorted(other.get_keys(coord_type))
            for key0 in self_keys:
                found_match = False
                for key1 in other_keys: 
                    if self[key0].equals(other[key1], rtol=rtol, atol=atol,
                                         traceback=False):

                        # Check the coordinates' transforms
                        if (hasattr(self, 'transforms') or
                            hasattr(other, 'transforms')):
                            if (hasattr(self, 'transforms') and
                                hasattr(other, 'transforms') and
                                (set(self.transforms) != 
                                 set([transform_map[t]
                                      for t in other.transforms]))):
                                # Both coordinates have transforms,
                                # but not the same ones.
                                continue
                        #--- End: if

                        found_match = True
                        other_keys.remove(key1)
                        break
                #--- End: for

                if not found_match:
                    if traceback:
                        print("%s: Different coordinate: %s" %
                              (self.__class__.__name__, repr(self[key0])))
                    return
            #--- End: for
        #--- End: for

        # ------------------------------------------------------------
        # Still here? Then the two spaces are equal
        # ------------------------------------------------------------
        return True
    #--- End: def

    def coord(self, arg, role=None, key=False, exact=False, dim=False, one_d=False,
              maximal_match=True):
        '''

Return a coordinate of the space.

Note that the returned coordinate is an object identity to the
coordinate stored in the space so, for example, a coordinate's
properties may be changed in-place:

>>> f.coord('height').long_name
AttributeError: Coordinate has no CF property 'long_name'
>>> f.coord('hei').long_name = 'HEIGHT'
>>> f.coord('heigh').long_name
'HEIGHT'

:Parameters:

    arg : str or dict or int
        The identify of the coordinate. One of:

        * A string containing (an abbreviation) of its standard name
          (such as 'time').

        * A string containing its identifier in the space (such as
          'dim2').

        * A dictionary containing one or more (abbreviations of)
          property names and their values as its key/value pairs (such
          as {'long_name': 'something'}). If two or more properties
          are specified then the returned coordinate must satisfy all
          criteria, unless `maximal_match` is False in which case the
          returned coordinate will satisfy all criteria at least one
          of the criteria.

        * An integer given the position of a dimension in the space's
          field's data array.

    exact : bool, optional
        If True then assume that the value of a name given by `arg` is
        exact. By default it is considered to be an abbreviation.

    key : bool, optional
        Return the space's identifier for the coordinate.

    role : str, optional
        Restrict the search to coordinates of the given role. Valid
        values are 'dim' and 'aux' for dimension and auxiliary
        coordinate constructs repectively. By default both types are
        considered.

:Returns:

    out : Coordinate or str or None
        The coordinate or, if `key` is True, the coordinate's
        identifier in the space or, if no unique coordinate could be
        found, None.

**Examples**

>>> s['dim2'].properties
{'_FillValue': None,
 'axis': 'X',
 'long_name': 'longitude',
 'standard_name': 'longitude',
 'units': 'degrees_east'}

>>> s.coord('longitude')
<CF Coordinate: longitude(360)>
>>> s.coord('longitude', exact=True)
<CF Coordinate: longitude(360)>
>>> s.coord('lon')
<CF Coordinate: longitude(360)>
>>> s.coord('long', key=True)
'dim2'

>>> s.coord('lon', exact=True)
None
>>> s.coord('lonX')
None

'''       
        if isinstance(arg, basestring) and arg in self:
            result_key = arg
            if key:
                return result_key
            elif dim:
                if one_d:
                    return self.dimensions[result_key][0]
                else:
                    return self.dimensions[result_key][:]
            else:
                return self[result_key]

        elif isinstance(arg, (int, long)):
            try:
                result_key = self.dimensions['data'][arg]
            except (IndexError, KeyError):
                return None
            
            if result_key not in self:
                return None
            
            if one_d and len(self.dimensions[result_key]) > 1:
                return None

            if key:                        
                return result_key            
            elif dim:
                if one_d:
                    return self.dimensions[result_key][0]
                else:
                    return self.dimensions[result_key][:]
            else:
                return self[result_key]
        #--- End: if

        # Still here?
        if isinstance(arg, basestring):
            arg = {'standard_name': arg}

        if role is None:
            space_keys = self.get_keys('^dim|^aux')
        elif role == 'dim':
            space_keys = self.get_keys('^dim')
        elif role == 'aux':
            space_keys = self.get_keys('^aux')       

        result_key = None

        for space_key in space_keys:

            found_coordinate = False

            for name, value in arg.iteritems():
           
                x = self[space_key].getprop(name, None)

                match = (x is not None and 
                         (exact and x == value) or
                         (not exact and 
                          ((isinstance(x, str)   and x.startswith(value)) or
                           (isinstance(x, Units) and x.equivalent(value)) or
                           x == value))
                         )

                if match:
                    found_coordinate = True
                    if not maximal_match:
                        break
                
                elif maximal_match:
                    found_coordinate = False
                    break
            #--- End: for

            if found_coordinate:
                if result_key is not None:
                    # Found two coordinates which match, so return
                    # None
                    return None

                result_key = space_key
            #--- End: if
        #--- End: for

        if result_key is None:
            # Found no coordinates which match, so return None
            return None

        if one_d and len(self.dimensions[result_key]) > 1:
            return None

        # Still here? Then found exactly one coordinate which matches,
        # so return it or its key
        if key:
            return result_key
        elif dim:
            if one_d:
                return self.dimensions[result_key][0]
            else:
                return self.dimensions[result_key][:]
        else:
            return self[result_key]
    #--- End: def

#--- End: class
